Skip to content

feat: configurable sensor debounce with stop-then-verify#37

Open
bharvey88 wants to merge 2 commits into
betafrom
feat/configurable-debounce
Open

feat: configurable sensor debounce with stop-then-verify#37
bharvey88 wants to merge 2 commits into
betafrom
feat/configurable-debounce

Conversation

@bharvey88

@bharvey88 bharvey88 commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Version: 25.04.12.1

What does this implement/fix?

Adds a configurable "Sensor Debounce Seconds" number entity that prevents false sensor readings (caused by CPAP air intake waves) from causing erratic pump behavior.

How it works (stop-then-verify):

  1. Sensor triggers a stop condition → pump stops immediately (safe)
  2. Waits the configured debounce seconds
  3. Re-checks the sensor — if it cleared (was a wave bounce), restarts the pump automatically
  4. If still triggered, pump stays off (genuine reading confirmed)

This is safer than delaying the stop — the pump is never running while a stop condition is active.

Configuration:

  • Disabled by default — users must enable the entity in HA to use it
  • Default 0 = original behavior (no debounce, immediate stop, no restart)
  • Range 0-30 seconds — adjustable from HA dashboard, no reflash needed

Safety:

  • Safety timeout (max_safe_run_time) is preserved across debounce restarts — a bouncy sensor cannot keep the pump running indefinitely
  • Debounce restarts bypass pump start condition checks (which could fail during bouncing) via a debounce_restart global flag

Also included:

  • Fix typo in output sensor entity ID (fulid_output_sensorfluid_output_sensor)
  • Auto-refill trigger (on_release) respects debounce delay with check-wait-confirm pattern

Types of changes

  • Bugfix (fixed change that fixes an issue)
  • New feature (thanks!)
  • Breaking change (repair/feature that breaks existing functionality)
  • Dependency Update - Does not publish
  • Other - Does not publish
  • Website of github readme file update - Does not publish
  • Github workflows - Does not publish

Checklist / Checklijst:

  • The code change has been tested and works locally
  • The code change has not yet been tested

If user-visible functionality or configuration variables are added/modified:

  • Added/updated documentation for the web page

Add a "Sensor Debounce Seconds" number entity (disabled by default,
0-30s, default 0) that lets users configure how many consecutive
seconds a sensor must read wet/dry before the pump reacts. Default 0
preserves original behavior. Also fix typo in output sensor ID
(fulid → fluid) and add debounce to auto-refill trigger.
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

Walkthrough

Replaces counter-based debounce with per-sensor timestamp globals, adds a configurable sensor_debounce_seconds, fixes fluid_output_sensor typo, debounces auto-refill with auto_refill_after_debounce, and rewrites pump_safety_check to stop the pump only after timestamps remain in wet/dry states for the debounce interval.

Changes

Pump debounce & safety

Layer / File(s) Summary
Timestamp globals
Integrations/ESPHome/Core.yaml
Adds non-restored timestamp globals: output_wet_since, input_wet_since, input_dry_since initialized to 0.
Input sensor trigger & ID fix
Integrations/ESPHome/Core.yaml
Debounces fluid_input_sensor auto-refill by cancelling on press and starting auto_refill_after_debounce on release; fixes binary sensor id fluid_output_sensor.
Debounce configuration
Integrations/ESPHome/Core.yaml
Adds sensor_debounce_seconds template number (config, 0..30, default 0) used for debounce delays and safety timing.
Debounced auto-refill script & safety loop
Integrations/ESPHome/Core.yaml
Adds auto_refill_after_debounce script delaying for sensor_debounce_seconds then conditionally calling pump logic. Rewrites pump_safety_check to update *_since timestamps and use them for normal-mode tank-full, inverted/refill destination-full, and stop-when-dry conditions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • TrevorSchirmer

Poem

🐰 I count the ticks when sensors say they're wet,
Timestamps calm the chatter, debounce keeps the set,
A typo mended, refill waits a beat,
The pump now stops when the readings stay neat. 💧

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: configurable sensor debounce with stop-then-verify' accurately describes the main change: adding a configurable sensor debounce mechanism with timestamp-based verification for pump stop conditions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/configurable-debounce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Integrations/ESPHome/Core.yaml (1)

281-296: ⚠️ Potential issue | 🟠 Major

Queued on_release timers can fire after a wet bounce.

Line 283 queues a delayed refill check on every release, but a later wet transition does not cancel the earlier timer. In ESPHome, delayed actions in on_release automations are not automatically canceled when the sensor transitions back to on (on_press). A pattern like OFF → ON → OFF can still satisfy the first timer after less than the configured continuous OFF time, breaking the consecutive-seconds debounce contract.

⏱️ Suggested pattern
   - platform: gpio
     name: Fluid Input
     id: fluid_input_sensor
@@
     on_release:
       then:
-        - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;'
-        - if:
-            condition:
-              and:
-                - binary_sensor.is_off: fluid_input_sensor
-                - switch.is_on: auto_refill
-                - switch.is_on: tank_refill_mode
-                - switch.is_off: pump_control
-            then:
-              - logger.log: "Auto refill triggered - tank level low"
-              - text_sensor.template.publish:
-                  id: last_pump_action
-                  state: "Auto Refill Triggered"
-              - script.execute: pumpUntilFull
+        - script.execute: auto_refill_debounce
+    on_press:
+      then:
+        - script.stop: auto_refill_debounce

Add a restartable script elsewhere so each new edge replaces the old timer:

script:
  - id: auto_refill_debounce
    mode: restart
    then:
      - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;'
      - if:
          condition:
            and:
              - binary_sensor.is_off: fluid_input_sensor
              - switch.is_on: auto_refill
              - switch.is_on: tank_refill_mode
              - switch.is_off: pump_control
          then:
            - logger.log: "Auto refill triggered - tank level low"
            - text_sensor.template.publish:
                id: last_pump_action
                state: "Auto Refill Triggered"
            - script.execute: pumpUntilFull
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Integrations/ESPHome/Core.yaml` around lines 281 - 296, The on_release
handler currently delays and then runs the refill check without cancellation,
allowing queued timers to fire after a wet bounce; refactor by creating a
restartable script (e.g., id: auto_refill_debounce) that performs the delay
using id(sensor_debounce_seconds) and then checks the conditions (binary_sensor:
fluid_input_sensor, switches: auto_refill, tank_refill_mode, pump_control) and
executes the same actions (logger log, text_sensor.publish to last_pump_action,
script.execute: pumpUntilFull); replace the on_release delayed block with a call
to this script so each edge restarts/cancels the previous timer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Integrations/ESPHome/Core.yaml`:
- Around line 467-498: The output wet-count condition incorrectly requires
stop_pump_when_full to be OFF during tank_refill_mode; change the condition that
increments id(output_wet_count) for the refill path so it checks
id(tank_refill_mode).state && id(stop_pump_when_full).state &&
id(fluid_input_sensor).state (i.e., require the stop_pump_when_full switch to be
ON in refill mode) instead of the current negated check; adjust only the
condition in the lambda that increments id(output_wet_count) (referencing
stop_pump_when_full, tank_refill_mode, fluid_input_sensor, fluid_output_sensor,
and output_wet_count) so the input-sensor full-stop path is armed when refill
mode is active.

---

Outside diff comments:
In `@Integrations/ESPHome/Core.yaml`:
- Around line 281-296: The on_release handler currently delays and then runs the
refill check without cancellation, allowing queued timers to fire after a wet
bounce; refactor by creating a restartable script (e.g., id:
auto_refill_debounce) that performs the delay using id(sensor_debounce_seconds)
and then checks the conditions (binary_sensor: fluid_input_sensor, switches:
auto_refill, tank_refill_mode, pump_control) and executes the same actions
(logger log, text_sensor.publish to last_pump_action, script.execute:
pumpUntilFull); replace the on_release delayed block with a call to this script
so each edge restarts/cancels the previous timer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c28e2629-d1f3-404b-8100-f18df1ec2812

📥 Commits

Reviewing files that changed from the base of the PR and between 623f8cb and efb038e.

📒 Files selected for processing (1)
  • Integrations/ESPHome/Core.yaml

Comment thread Integrations/ESPHome/Core.yaml Outdated
Add a "Sensor Debounce Seconds" number entity (disabled by default,
0-30s, default 0) for water level sensors. Uses stop-then-verify
approach: pump stops immediately when a sensor triggers, waits the
configured seconds, then re-checks. If the reading was a false alarm
(e.g. wave bounce), the pump restarts automatically. Safety timeout
is preserved across debounce restarts. Also fix typo in output sensor
ID (fulid -> fluid) and add debounce to auto-refill trigger.
@bharvey88 bharvey88 changed the title feat: configurable sensor debounce for water level sensors feat: configurable sensor debounce with stop-then-verify Apr 12, 2026
@bharvey88 bharvey88 changed the title feat: configurable sensor debounce with stop-then-verify feat: configurable sensor debounce filter May 25, 2026
@bharvey88

Copy link
Copy Markdown
Contributor Author

Rewrote from stop-then-verify to a timestamp-based filter.

Why: the original mechanism cycled the pump on/off during sloshing at the fill threshold, which is exactly the case this feature is supposed to handle. It also silently broke max_safe_run_time: pump_control.on_turn_off unconditionally clears pump_start_time, and the debounce_restart branch in on_turn_on never restored it, so the safety timeout check short-circuited to false after any debounce restart.

New approach: pump_safety_check maintains <state>_since timestamps from live sensor readings each iteration. Each stop condition fires only after its sensor state has been sustained for sensor_debounce_seconds. Bounces reset the timestamp; the pump runs through them and stops cleanly once the signal settles. max_safe_run_time is independent and fires as designed.

CodeRabbit comments:

  • The on_release timer-cancellation concern (around old line 281-296) is resolved: auto-refill now uses a mode: restart script (auto_refill_after_debounce) with on_press: script.stop, so bouncing edges on fluid_input_sensor cancel any in-flight timer.
  • The earlier inline comment about output_wet_count polarity in tank_refill_mode referred to a counter-based draft that has been superseded by this rewrite. The new destination-full check uses input_wet_since and gates on tank_refill_mode: ON + stop_pump_when_full: OFF, which matches the inverted-mode contract.

Untested on hardware; CI builds the firmware. Flash test recommended before merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Integrations/ESPHome/Core.yaml (1)

521-532: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Inverted/refill mode stop condition will never trigger.

The condition at line 526 requires stop_pump_when_full to be OFF, but when tank_refill_mode is enabled (line 164-168), it automatically turns stop_pump_when_full ON. These conditions are mutually exclusive, so the "destination full" stop in refill mode can never fire.

The condition should check that stop_pump_when_full is ON (consistent with how it gates the normal-mode stop at line 503):

🐛 Proposed fix
             # Destination full in inverted/refill mode: input sensor wet for >= debounce_seconds
             - if:
                 condition:
                   - and:
                       - switch.is_on: tank_refill_mode
-                      - switch.is_off: stop_pump_when_full
+                      - switch.is_on: stop_pump_when_full
                       - lambda: |-
                           return id(input_wet_since) != 0
                                  && (millis() - id(input_wet_since)) / 1000 >= (uint32_t)id(sensor_debounce_seconds).state;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Integrations/ESPHome/Core.yaml` around lines 521 - 532, The
inverted/refill-mode stop condition is impossible because it checks
switch.is_off: stop_pump_when_full while tank_refill_mode sets
stop_pump_when_full on; change the condition to require switch.is_on:
stop_pump_when_full so the block that checks id(input_wet_since) and (millis() -
id(input_wet_since)) / 1000 >= (uint32_t)id(sensor_debounce_seconds).state can
execute and then run the logger and switch.turn_off: pump_control; update the
condition containing tank_refill_mode and stop_pump_when_full accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Integrations/ESPHome/Core.yaml`:
- Around line 521-532: The inverted/refill-mode stop condition is impossible
because it checks switch.is_off: stop_pump_when_full while tank_refill_mode sets
stop_pump_when_full on; change the condition to require switch.is_on:
stop_pump_when_full so the block that checks id(input_wet_since) and (millis() -
id(input_wet_since)) / 1000 >= (uint32_t)id(sensor_debounce_seconds).state can
execute and then run the logger and switch.turn_off: pump_control; update the
condition containing tank_refill_mode and stop_pump_when_full accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81f46284-dc68-4ba8-9ecb-d660c7972d02

📥 Commits

Reviewing files that changed from the base of the PR and between efb038e and 90b062c.

📒 Files selected for processing (1)
  • Integrations/ESPHome/Core.yaml

@bharvey88

Copy link
Copy Markdown
Contributor Author

Customer-reported regression: in refill mode, the pump did not stop when the output sensor went wet, and a tank overflowed.

Cause: the previous timestamp-refactor commit added switch.is_off: tank_refill_mode to the "stop when full" check. The pre-PR code never had that gate. In refill mode, stop_pump_when_full is ON (set by on_boot and by pumpUntilFull), so the stop check required tank_refill_mode: OFF and never fired. The pump kept running through wet readings.

Fix: removed the gate. The three stop branches are already mutually exclusive via stop_pump_when_full and stop_pump_when_dry switch states.

This is independent of the sensor-bouncing concern the customer also raised. If filtering still feels inadequate after this fix, the next thing to investigate is the 1Hz poll rate inside pump_safety_check's while loop, which can phase-lock against sub-second sloshing. That would be a follow-up.

Pushed in 889ebda. Recommend reflashing before any further refill use.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@bharvey88 bharvey88 force-pushed the feat/configurable-debounce branch from 889ebda to 52a6748 Compare May 29, 2026 16:37
@bharvey88

Copy link
Copy Markdown
Contributor Author

Reverted via force-push: branch is back to 52a6748 (the stop-then-verify version, pre-rewrite).

My timestamp-refactor commits (90b062c, 889ebda) failed in customer testing. Going back to known state so the customer can reflash and use the device. We'll re-attempt the debounce work later with better test coverage.

@bharvey88 bharvey88 changed the title feat: configurable sensor debounce filter feat: configurable sensor debounce with stop-then-verify May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants